﻿using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Content;
using System;

namespace Engine2D
{
    class Trackball
    {
        private Camera mCamera;
        private Vector3 mWorldPosition;
        private Vector3 mWorldSize;

        private Vector2 mScreenPosition;
        private Vector2 mScreenSize;

        // for debug/reference drawing
        private Model mTrackballModel;
        private Vector3 mLastMapped;
        private Matrix mRotation;

        public float Sensitivity { get; set; }

        public Vector3 WorldPosition
        {
            get { return mWorldPosition; }
            set
            {
                mWorldPosition = value;
                UpdateScreenValues();
            }
        }

        public Vector3 WorldSize
        {
            get { return mWorldSize; }
            set
            {
                mWorldSize = value;
                UpdateScreenValues();
            }
        }

        public Camera Camera
        {
            get { return mCamera; }
            set
            {
                mCamera = value;
                UpdateScreenValues();
            }
        }

        public Trackball(ContentManager content)
        {
            mCamera = null;
            mWorldPosition = Vector3.Zero;
            mWorldSize = Vector3.One;

            mScreenPosition = Vector2.Zero;
            mScreenSize = Vector2.One;

            mTrackballModel = content.Load<Model>("unitsphere");
            mLastMapped = Vector3.Zero;
            mRotation = Matrix.Identity;

            Sensitivity = 2.0f;
        }

        private void UpdateScreenValues()
        {
            if (mCamera != null)
            {
                Vector3 screenPos = mCamera.Viewport.Project(mWorldPosition, Camera.Projection, Camera.View, Matrix.Identity);
                mScreenPosition = new Vector2(screenPos.X, screenPos.Y);

                Vector3 screenPosTL = Camera.Viewport.Project(-mWorldSize / 2.0f, Camera.Projection, Camera.View, Matrix.CreateTranslation(mWorldPosition));
                Vector3 screenPosBR = Camera.Viewport.Project(mWorldSize / 2.0f, Camera.Projection, Camera.View, Matrix.CreateTranslation(mWorldPosition));
                mScreenSize = new Vector2(screenPosBR.X - screenPosTL.X, screenPosTL.Y - screenPosBR.Y);

                mRotation = Matrix.Identity;
            }
        }

        public void Draw(GraphicsDevice device)
        {
            device.DepthStencilState = DepthStencilState.None;

            RasterizerState rasterizerState = new RasterizerState();
            //rasterizerState.CullMode = CullMode.CullCounterClockwiseFace;
            //rasterizerState.FillMode = FillMode.WireFrame;
            device.RasterizerState = rasterizerState;

            foreach (ModelMesh mesh in mTrackballModel.Meshes)
            {
                foreach (BasicEffect effect in mesh.Effects)
                {
                    //effect.EnableDefaultLighting();
                    effect.View = Camera.View;
                    effect.Projection = Camera.Projection;
                    effect.World = Matrix.CreateScale(mWorldSize) * mRotation * Matrix.CreateTranslation(mWorldPosition);
                }
                mesh.Draw();
            }
        }

        private Vector3 Map(Vector2 point)
        {
            // make point relative to the centre
            point -= mScreenPosition;

            Vector3 v;
            float d;
            v.X = point.X * 2.0f / mScreenSize.X;
            v.Y = -point.Y * 2.0f / mScreenSize.Y;
            v.Z = 0.0f;
            d = v.Length();
            d = (d < 1.0f) ? d : 1.0f;
            v.Z = (float)Math.Sqrt(1.001 - d * d);
            v.Normalize(); // Still need to normalize, since we only capped d, not v.

            return v;
        }

        public void StartRotate(Vector2 point)
        {
            mLastMapped = Map(point);
        }

        public Matrix Rotate(Vector2 point)
        {
            Matrix rotation = Matrix.Identity;

            Vector3 mapped = Map(point);
            float distance = (mLastMapped - mapped).Length();
            if (distance > 0.0001f) // ignore tiny movements
            {
                Vector3 rotAxis = Vector3.Cross(mLastMapped, mapped);
                rotAxis.Normalize();
                rotation = Matrix.CreateFromAxisAngle(rotAxis, distance * Sensitivity);

                mRotation *= rotation;
            }

            mLastMapped = mapped;
            return rotation;
        }
    }
}
